#!/usr/bin/python3

# vim: set filetype=python:

import argparse
import os
import os.path
import sys
#import itertools
#import collections
import datetime
import terminaltables
import shutil
import subprocess

import yaml
import csv

from typing import Optional, Any
from typing import Dict

def main():
    #  Commandline Arguments {{{ # 
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="command")

    init_parser = subparsers.add_parser("init", help="Initiate a version repository.")
    init_parser.add_argument("author", help="Author name.")

    list_parser = subparsers.add_parser("list", help="List tracked files.")

    check_parser = subparsers.add_parser("check", help="Check the versions of a specific file.")
    check_parser.add_argument("--version", "-v", action="store_true", help="Check a version number instead of a file name.")
    check_parser.add_argument("file", type=str, help="File or version which versions needs check.")

    diff_parser = subparsers.add_parser("diff", help="Check the difference between two versions. If version b is ignored, the current workspace file is compared.")
    diff_parser.add_argument("--versiona", "-a", type=str, help="Version to compare.")
    diff_parser.add_argument("--versionb", "-b", type=str, help="Version to compare.")
    diff_parser.add_argument("file", type=str, help="File to check.")

    status_parser = subparsers.add_parser("status", help="Check all the differences of tracked files.")

    commit_parser = subparsers.add_parser("commit", help="Commit a file version.")
    commit_parser.add_argument("file", nargs="+", type=str, help="File to commit.")
    commit_parser.add_argument("--version", "-v", type=str, required=True, help="File version.")
    commit_parser.add_argument("--comment", "-m", type=str, required=True, help="Version comment.")

    checkout_parser = subparsers.add_parser("checkout", help="Checkout a file version")
    checkout_parser.add_argument("file", type=str, help="File to checkout.")
    checkout_parser.add_argument("version", type=str, help="Version to checkout.")

    log_parser = subparsers.add_parser("log", help="Print the daily log.")
    log_parser.add_argument("date", nargs="*", type=int,
        help="Date which log to be checked. Specify by \"Y M D\", \"M D\", \"M\", or ignore.")

    export_parser = subparsers.add_parser("export", help="Export the version table.")
    export_parser.add_argument("--file", "-f", type=str, required=False,
        help="File which version is to be exported.")
    export_parser.add_argument("--output", "-o", type=str, required=False,
        help="Markdown/CSV file to output.")

    args = parser.parse_args()
    #  }}} Commandline Arguments # 

    #  Constants {{{ # 
    VERSION_FOLDER = "version.folder"
    IMAGE_FOLDER = "versions"
    IMAGE_TEMPLATE = "{:}_v{:}{:}"
    VERSION_INDEX = "version.index.yaml"
    VERSION_LOG = "version.log.md"

    VERSION_FOLDER_EXISTS_CODE = -1
    NOT_VERSION_REPOSITORY_CODE = -2
    DATE_PARSING_CODE = -3
    FILE_NOT_TRACKED_CODE = -4
    VERSION_EXISTS_CODE = -5
    VERSION_NOT_EXISTS_CODE = -6
    #  }}} Constants # 

    #  Initiation {{{ # 
    if args.command=="init":
        if os.path.exists(VERSION_FOLDER):
            print("Version Folder already initiated!", file=sys.stderr)
            return VERSION_FOLDER_EXISTS_CODE

        os.makedirs(os.path.join(VERSION_FOLDER, IMAGE_FOLDER))
        with open(os.path.join(VERSION_FOLDER, "config"), "w") as f:
            yaml.dump({"author": args.author}, f, yaml.Dumper, allow_unicode=True)
        with open(os.path.join(VERSION_FOLDER, VERSION_INDEX), "w") as f:
            yaml.dump(dict(), f, yaml.Dumper, allow_unicode=True)
        with open(os.path.join(VERSION_FOLDER, VERSION_LOG), "w") as f:
            f.write("# {:}\n".format(os.path.basename(os.path.abspath("."))))

        return 0
    #  }}} Initiation # 

    basepath = "."
    while os.path.exists(basepath) and VERSION_FOLDER not in os.listdir(basepath):
        basepath = os.path.join("..", basepath)
    if not os.path.exists(basepath):
        print("This path is not under a version repository yet!", file=sys.stderr)
        return NOT_VERSION_REPOSITORY_CODE

    #  Public Functions {{{ # 
    def diff( arg_file: str
            , versiona: Optional[str], versionb: Optional[str] = None
            , prints_name: bool = False
            ) -> int:
        """
        Args:
            arg_file (str): path to target file, relative to the working path
              (not base path)
            versiona (Optional[str]): the first version to compare, None for
              the last version
            versionb (Optional[str]): the second version to compare, None for
              the workspace version
            prints_name (bool): if the file name should be print

        Returns:
            int: status code
        """

        file: str = os.path.relpath(arg_file, basepath)
        main_name: str
        extension: str
        main_name, extension = os.path.splitext(file)

        if not versiona:
            with open(os.path.join(basepath, VERSION_FOLDER, VERSION_INDEX)) as f:
                indices: Dict[str, Any] = yaml.load(f, yaml.Loader)
            if file not in indices:
                print("File Not Tracked Yet!", file=sys.stderr)
                return FILE_NOT_TRACKED_CODE
            versiona: str = indices[file]["last_version"]
        filea: str = os.path.join( basepath, VERSION_FOLDER, IMAGE_FOLDER
                                 , IMAGE_TEMPLATE.format(main_name, versiona, extension)
                                 )

        fileb: str = os.path.join( basepath, VERSION_FOLDER, IMAGE_FOLDER
                                 , IMAGE_TEMPLATE.format(main_name, versionb, extension)
                                 ) if versionb\
                                 else arg_file
        if not os.path.exists(filea) or not os.path.exists(fileb):
            print("Version Not Exists!", file=sys.stderr)
            return VERSION_NOT_EXISTS_CODE
        result: subprocess.CompletedProcess = subprocess.run(["diff", "--color=always", filea, fileb], capture_output=True, text=True)
        if len(result.stdout)>0:
            if prints_name:
                print("{:}: {:} vs {:}".format(file, versiona, versionb or "WORKSPACE"))
            print(result.stdout)
        return 0
    #  }}} Public Functions # 

    if args.command=="list":
        #  List Command {{{ # 
        with open(os.path.join(basepath, VERSION_FOLDER, VERSION_INDEX)) as f:
            filelist = list(sorted(yaml.load(f, yaml.Loader).keys()))
        for f in filelist:
            print(f)
        return 0
        #  }}} List Command # 

    if args.command=="log":
        #  Log Function {{{ # 
        try:
            if len(args.date)==0:
                date = datetime.date.today()
            if len(args.date)>=3:
                year, month, day = map(int, args.date[:3])
                date = datetime.date(year, month, day)
            elif len(args.date)==2:
                month, day = map(int, args.date)
                year = datetime.date.today().year
                date = datetime.date(year, month, day)
            elif len(args.date)==1:
                month = int(args.date[0])
                today = datetime.date.today()
                date = datetime.date(today.year, month, today.day)
        except:
            print("Date parsing error!", file=sys.stderr)
            return DATE_PARSING_CODE

        date_str = date.isoformat()
        with open(os.path.join(basepath, VERSION_FOLDER, VERSION_LOG)) as f:
            found = False
            for l in f:
                l = l.strip()
                if l.startswith("###"):
                    if found:
                        break
                    if l[4:]==date_str:
                        found = True
                if found:
                    print(l)

        return 0
        #  }}} Log Function # 

    if args.command=="export":
        #  Exporting {{{ # 
        file = os.path.relpath(args.file, basepath) if args.file is not None\
                else None
        with open(os.path.join(basepath, VERSION_FOLDER, VERSION_INDEX)) as f:
            indices = yaml.load(f, yaml.Loader)
            if file is not None:
                if file not in indices:
                    print("File Not Tracked Yet!", file=sys.stderr)
                    return FILE_NOT_TRACKED_CODE
                indices = {file: indices[file]}

        table_data = [["File", "Version", "Last Version", "Date", "Author", "Comment"]]
        for f in sorted(indices.keys()):
            last_version = indices[f]["last_version"]
            for rcd in indices[f]["versions"]:
                table_data.append([
                        f,
                        rcd["version"],
                        "v" if rcd["version"]==last_version else "",
                        datetime.date.fromtimestamp(rcd["date"]).isoformat(),
                        rcd["author"],
                        rcd["comment"]
                    ])
        if args.output is None:
            table = terminaltables.AsciiTable(table_data)
            print(table.table)
        elif args.output.endswith(".md"):
            with open(args.output, "w") as f:
                table = terminaltables.GithubFlavoredMarkdownTable(table_data)
                f.write(table.table)
                f.write("\n")
        elif args.output.endswith(".csv"):
            with open(args.output, "w") as f:
                writer = csv.writer(f)
                writer.writerows(table_data)

        return 0
        #  }}} Exporting # 

    if args.command=="commit":
        #  Action commit {{{ # 
        for f in args.file:
            file = os.path.relpath(f, basepath)

            # 1. update image
            main_name, extension = os.path.splitext(file)
            backup_file = os.path.join( basepath, VERSION_FOLDER, IMAGE_FOLDER
                                      , IMAGE_TEMPLATE.format( main_name
                                                             , args.version
                                                             , extension
                                                             )
                                      )
            if os.path.exists(backup_file):
                print("Version Exists!", file=sys.stderr)
                return VERSION_NOT_EXISTS_CODE
            if not os.path.exists(os.path.dirname(backup_file)):
                os.makedirs(os.path.dirname(backup_file))
            shutil.copy2(f, backup_file)

            # 2. update index
            index_file = os.path.join(basepath, VERSION_FOLDER, VERSION_INDEX)
            with open(os.path.join(basepath, VERSION_FOLDER, "config")) as cfg:
                author = yaml.load(cfg, yaml.Loader)["author"]
            with open(index_file) as idx:
                indices = yaml.load(idx, yaml.Loader)
            is_new_file = False
            if file not in indices:
                indices[file] = {
                        "last_version": "",
                        "versions": []
                    }
                is_new_file = True
            indices[file]["last_version"] = args.version
            indices[file]["versions"].append({
                    "version": args.version,
                    "date": os.path.getmtime(f),
                    "author": author,
                    "comment": args.comment
                })
            shutil.copy(index_file, index_file + ".bak")
            with open(index_file, "w") as idx:
                yaml.dump(indices, idx, yaml.Dumper, allow_unicode=True)
            os.remove(index_file + ".bak")

            # 3. update log
            today_str = datetime.date.today().isoformat()
            mdate_str = datetime.date.fromtimestamp(os.path.getmtime(f)).isoformat()
            cdate_str = datetime.date.fromtimestamp(os.path.getctime(f)).isoformat()
            if today_str!=mdate_str:
                status = "T"
            elif mdate_str==cdate_str:
                status = "C"
            elif is_new_file:
                status = "A"
            else:
                status = "M"

            log_file = os.path.join(basepath, VERSION_FOLDER, VERSION_LOG)
            bak_file = log_file + ".bak"
            shutil.copy(log_file, bak_file)
            with open(bak_file) as r_f,\
                    open(log_file, "w") as wr_f:
                found_today = False
                found_file = False
                previous_statuses = ""
                for l in r_f:
                    l = l.strip()
                    if found_today and not found_file and l.startswith("*"):
                        items = l.split()
                        if items[-1]==file:
                            found_file = True
                            previous_statuses = items[1]
                            continue
                    if l.startswith("###"):
                        if l[4:]==today_str:
                            found_today = True
                    wr_f.write(l + "\n")
                if not found_today:
                    wr_f.write("\n### {:}\n\n".format(today_str))
                wr_f.write("* {:}{:} {:}\n".format(previous_statuses, status, file))
            os.remove(bak_file)
        return 0
        #  }}} Action commit # 

    if args.command=="check":
        #  Action check {{{ # 
        with open(os.path.join(basepath, VERSION_FOLDER, VERSION_INDEX)) as f:
            indices = yaml.load(f, yaml.Loader)
        table_data = [["File", "Version", "Last Version", "Date", "Author", "Comment"]]

        if args.version:
            for f, vsns in indices.items():
                last_version = vsns["last_version"]
                for vsn in vsns["versions"]:
                    if vsn["version"]==args.file:
                        table_data.append([ f
                                          , args.file
                                          , "v" if args.file==last_version else ""
                                          , datetime.date.fromtimestamp(vsn["date"]).isoformat()
                                          , vsn["author"]
                                          , vsn["comment"]
                                          ])
                        break
        else:
            file = os.path.relpath(args.file, basepath)

            if file not in indices:
                print("File Not Tracked Yet!", file=sys.stderr)
                return FILE_NOT_TRACKED_CODE
            last_version = indices[file]["last_version"]
            table_data += map(lambda rcd: [
                                file,
                                rcd["version"],
                                "v" if rcd["version"]==last_version else "",
                                datetime.date.fromtimestamp(rcd["date"]).isoformat(),
                                rcd["author"],
                                rcd["comment"]
                            ],
                        indices[file]["versions"])

        table = terminaltables.AsciiTable(table_data)
        print(table.table)
        return 0
        #  }}} Action check # 

    if args.command=="diff":
        return diff(args.file, args.versiona, args.versionb)

    if args.command=="status":
        #  Status Command {{{ # 
        with open(os.path.join(basepath, VERSION_FOLDER, VERSION_INDEX)) as f:
            indices: Dict[str, Any] = yaml.load(f, yaml.Loader)

        for f in indices:
            diff(os.path.join(basepath, f), None, prints_name=True)

        return 0
        #  }}} Status Command # 

    if args.command=="checkout":
        #  Action checkout {{{ # 
        file = os.path.relpath(args.file, basepath)
        main_name, extension = os.path.splitext(file)
        backup_file = os.path.join( basepath, VERSION_FOLDER, IMAGE_FOLDER
                                  , IMAGE_TEMPLATE.format(main_name, args.version, extension)
                                  )
        if not os.path.exists(backup_file):
            print("Version Not Exists!", file=sys.stderr)
            return VERSION_NOT_EXISTS_CODE
        if os.path.exists(args.file):
            os.remove(args.file)
        shutil.copy2(backup_file, args.file)

        with open(os.path.join(basepath, VERSION_FOLDER, VERSION_INDEX)) as f:
            indices = yaml.load(f, yaml.Loader)
        indices[file]["last_version"] = args.version
        with open(os.path.join(basepath, VERSION_FOLDER, VERSION_INDEX), "w") as f:
            yaml.dump(indices, f, yaml.Dumper, allow_unicode=True)

        return 0
        #  }}} Action checkout # 

if __name__ == "__main__":
    exit(main())
